/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.ranger.plugin.policyengine;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import org.apache.commons.lang.StringUtils;
import org.apache.ranger.plugin.model.RangerPolicy;
import org.apache.ranger.plugin.model.RangerPolicy.RangerPolicyItemDataMaskInfo;
import org.apache.ranger.plugin.model.RangerPolicy.RangerPolicyItemRowFilterInfo;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import static org.apache.ranger.plugin.policyevaluator.RangerPolicyEvaluator.ACCESS_ALLOWED;
import static org.apache.ranger.plugin.policyevaluator.RangerPolicyEvaluator.ACCESS_CONDITIONAL;
import static org.apache.ranger.plugin.policyevaluator.RangerPolicyEvaluator.ACCESS_DENIED;

public class RangerResourceACLs {
    private final Map<String, Map<String, AccessResult>> userACLs   = new HashMap<>();
    private final Map<String, Map<String, AccessResult>> groupACLs  = new HashMap<>();
    private final Map<String, Map<String, AccessResult>> roleACLs   = new HashMap<>();
    private final List<RowFilterResult>                  rowFilters = new ArrayList<>();
    private final List<DataMaskResult>                   dataMasks  = new ArrayList<>();
    private final Set<String>                            datasets   = new HashSet<>();
    private final Set<String>                            projects   = new HashSet<>();

    public RangerResourceACLs() {
    }

    public Map<String, Map<String, AccessResult>> getUserACLs() {
        return userACLs;
    }

    public Map<String, Map<String, AccessResult>> getGroupACLs() {
        return groupACLs;
    }

    public Map<String, Map<String, AccessResult>> getRoleACLs() {
        return roleACLs;
    }

    public List<RowFilterResult> getRowFilters() {
        return rowFilters;
    }

    public List<DataMaskResult> getDataMasks() {
        return dataMasks;
    }

    public Set<String> getDatasets() {
        return datasets;
    }

    public Set<String> getProjects() {
        return projects;
    }

    public void finalizeAcls() {
        Map<String, AccessResult> publicGroupAccessInfo = groupACLs.get(RangerPolicyEngine.GROUP_PUBLIC);

        if (publicGroupAccessInfo != null) {
            for (Map.Entry<String, AccessResult> entry : publicGroupAccessInfo.entrySet()) {
                String       accessType   = entry.getKey();
                AccessResult accessResult = entry.getValue();
                int          access       = accessResult.getResult();

                if (access == ACCESS_DENIED || access == ACCESS_ALLOWED) {
                    for (Map.Entry<String, Map<String, AccessResult>> mapEntry : userACLs.entrySet()) {
                        Map<String, AccessResult> mapValue          = mapEntry.getValue();
                        AccessResult              savedAccessResult = mapValue.get(accessType);
                        if (savedAccessResult != null && !savedAccessResult.getIsFinal()) {
                            mapValue.remove(accessType);
                        }
                    }

                    for (Map.Entry<String, Map<String, AccessResult>> mapEntry : groupACLs.entrySet()) {
                        if (!StringUtils.equals(mapEntry.getKey(), RangerPolicyEngine.GROUP_PUBLIC)) {
                            Map<String, AccessResult> mapValue          = mapEntry.getValue();
                            AccessResult              savedAccessResult = mapValue.get(accessType);
                            if (savedAccessResult != null && !savedAccessResult.getIsFinal()) {
                                mapValue.remove(accessType);
                            }
                        }
                    }
                }
            }
        }
        finalizeAcls(userACLs);
        finalizeAcls(groupACLs);
        finalizeAcls(roleACLs);
    }

    public void setUserAccessInfo(String userName, String accessType, Integer access, RangerPolicy policy) {
        Map<String, AccessResult> userAccessInfo = userACLs.computeIfAbsent(userName, k -> new HashMap<>());

        AccessResult accessResult = userAccessInfo.get(accessType);

        if (accessResult == null) {
            accessResult = new AccessResult(access, policy);

            userAccessInfo.put(accessType, accessResult);
        } else if (!ACCESS_CONDITIONAL.equals(access)) {
            accessResult.setResult(access);
            accessResult.setPolicy(policy);
        }
    }

    public void setGroupAccessInfo(String groupName, String accessType, Integer access, RangerPolicy policy) {
        Map<String, AccessResult> groupAccessInfo = groupACLs.computeIfAbsent(groupName, k -> new HashMap<>());

        AccessResult accessResult = groupAccessInfo.get(accessType);

        if (accessResult == null) {
            accessResult = new AccessResult(access, policy);

            groupAccessInfo.put(accessType, accessResult);
        } else if (!ACCESS_CONDITIONAL.equals(access)) {
            accessResult.setResult(access);
            accessResult.setPolicy(policy);
        }
    }

    public void setRoleAccessInfo(String roleName, String accessType, Integer access, RangerPolicy policy) {
        Map<String, AccessResult> roleAccessInfo = roleACLs.computeIfAbsent(roleName, k -> new HashMap<>());

        AccessResult accessResult = roleAccessInfo.get(accessType);

        if (accessResult == null) {
            accessResult = new AccessResult(access, policy);

            roleAccessInfo.put(accessType, accessResult);
        } else if (!ACCESS_CONDITIONAL.equals(access)) {
            accessResult.setResult(access);
            accessResult.setPolicy(policy);
        }
    }

    @Override
    public int hashCode() {
        return Objects.hash(userACLs, groupACLs, roleACLs, rowFilters, dataMasks);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        } else if (obj == null || !getClass().equals(obj.getClass())) {
            return false;
        } else {
            RangerResourceACLs other = (RangerResourceACLs) obj;

            return Objects.equals(userACLs, other.userACLs) &&
                    Objects.equals(groupACLs, other.groupACLs) &&
                    Objects.equals(roleACLs, other.roleACLs) &&
                    Objects.equals(rowFilters, other.rowFilters) &&
                    Objects.equals(dataMasks, other.dataMasks) &&
                    Objects.equals(datasets, other.datasets) &&
                    Objects.equals(projects, other.projects);
        }
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();

        sb.append("{");

        sb.append("UserACLs={");
        for (Map.Entry<String, Map<String, AccessResult>> entry : userACLs.entrySet()) {
            sb.append("user=").append(entry.getKey()).append(":");
            sb.append("permissions={");
            for (Map.Entry<String, AccessResult> permission : entry.getValue().entrySet()) {
                sb.append("{Permission=").append(permission.getKey()).append(", value=").append(permission.getValue()).append("},");
                sb.append("{RangerPolicyID=").append(permission.getValue().getPolicy() == null ? null : permission.getValue().getPolicy().getId()).append("},");
            }
            sb.append("},");
        }
        sb.append("}");

        sb.append(", GroupACLs={");
        for (Map.Entry<String, Map<String, AccessResult>> entry : groupACLs.entrySet()) {
            sb.append("group=").append(entry.getKey()).append(":");
            sb.append("permissions={");
            for (Map.Entry<String, AccessResult> permission : entry.getValue().entrySet()) {
                sb.append("{Permission=").append(permission.getKey()).append(", value=").append(permission.getValue()).append("}, ");
                sb.append("{RangerPolicy ID=").append(permission.getValue().getPolicy() == null ? null : permission.getValue().getPolicy().getId()).append("},");
            }
            sb.append("},");
        }
        sb.append("}");

        sb.append(", RoleACLs={");
        for (Map.Entry<String, Map<String, AccessResult>> entry : roleACLs.entrySet()) {
            sb.append("role=").append(entry.getKey()).append(":");
            sb.append("permissions={");
            for (Map.Entry<String, AccessResult> permission : entry.getValue().entrySet()) {
                sb.append("{Permission=").append(permission.getKey()).append(", value=").append(permission.getValue()).append("}, ");
                sb.append("{RangerPolicy ID=").append(permission.getValue().getPolicy() == null ? null : permission.getValue().getPolicy().getId()).append("},");
            }
            sb.append("},");
        }
        sb.append("}");

        sb.append("}");

        sb.append(", rowFilters=[");
        for (RowFilterResult rowFilter : rowFilters) {
            rowFilter.toString(sb);
            sb.append(" ");
        }
        sb.append("]");

        sb.append(", dataMasks=[");
        for (DataMaskResult dataMask : dataMasks) {
            dataMask.toString(sb);
            sb.append(" ");
        }
        sb.append("]");

        sb.append(", datasets=[");
        for (String dataset : datasets) {
            sb.append(dataset).append(" ");
        }
        sb.append("]");

        sb.append(", projects=[");
        for (String project : projects) {
            sb.append(project).append(" ");
        }
        sb.append("]");

        return sb.toString();
    }

    private void finalizeAcls(Map<String, Map<String, AccessResult>> acls) {
        List<String> keysToRemove = new ArrayList<>();
        for (Map.Entry<String, Map<String, AccessResult>> entry : acls.entrySet()) {
            if (entry.getValue().isEmpty()) {
                keysToRemove.add(entry.getKey());
            } else {
                for (Map.Entry<String, AccessResult> permission : entry.getValue().entrySet()) {
                    permission.getValue().setIsFinal(true);
                }
            }
        }
        for (String keyToRemove : keysToRemove) {
            acls.remove(keyToRemove);
        }
    }

    @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class AccessResult {
        private int          result;
        private boolean      isFinal;
        private RangerPolicy policy;

        public AccessResult() {
            this(-1, null);
        }

        public AccessResult(int result, RangerPolicy policy) {
            this(result, false, policy);
        }

        public AccessResult(int result, boolean isFinal, RangerPolicy policy) {
            setIsFinal(isFinal);
            setResult(result);
            setPolicy(policy);
        }

        public int getResult() {
            return result;
        }

        public void setResult(int result) {
            if (!isFinal) {
                this.result = result;

                if (this.result == ACCESS_DENIED) {
                    isFinal = true;
                }
            }
        }

        public boolean getIsFinal() {
            return isFinal;
        }

        public void setIsFinal(boolean isFinal) {
            this.isFinal = isFinal;
        }

        public RangerPolicy getPolicy() {
            return policy;
        }

        public void setPolicy(RangerPolicy policy) {
            this.policy = policy;
        }

        @Override
        public boolean equals(Object other) {
            if (other == null) {
                return false;
            }

            if (other instanceof AccessResult) {
                AccessResult otherObject = (AccessResult) other;
                return result == otherObject.result && isFinal == otherObject.isFinal;
            } else {
                return false;
            }
        }

        @Override
        public int hashCode() {
            return Objects.hash(result, isFinal); // policy is not relevant for equals() and hashCode()
        }

        @Override
        public String toString() {
            if (result == ACCESS_ALLOWED) {
                return "ALLOWED, final=" + isFinal;
            }
            if (result == ACCESS_DENIED) {
                return "NOT_ALLOWED, final=" + isFinal;
            }
            return "CONDITIONAL_ALLOWED, final=" + isFinal;
        }
    }

    @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class DataMaskResult implements Serializable {
        private static final long serialVersionUID = 1L;

        private final Set<String>                  users;
        private final Set<String>                  groups;
        private final Set<String>                  roles;
        private final Set<String>                  accessTypes;
        private final RangerPolicyItemDataMaskInfo maskInfo;
        private       boolean                      isConditional;

        public DataMaskResult(Set<String> users, Set<String> groups, Set<String> roles, Set<String> accessTypes, RangerPolicyItemDataMaskInfo maskInfo) {
            this.users       = users;
            this.groups      = groups;
            this.roles       = roles;
            this.accessTypes = accessTypes;
            this.maskInfo    = maskInfo;
        }

        public DataMaskResult(DataMaskResult that) {
            this.users         = that.users;
            this.groups        = that.groups;
            this.roles         = that.roles;
            this.accessTypes   = that.accessTypes;
            this.maskInfo      = that.maskInfo;
            this.isConditional = that.isConditional;
        }

        public Set<String> getUsers() {
            return users;
        }

        public Set<String> getGroups() {
            return groups;
        }

        public Set<String> getRoles() {
            return roles;
        }

        public Set<String> getAccessTypes() {
            return accessTypes;
        }

        public RangerPolicyItemDataMaskInfo getMaskInfo() {
            return maskInfo;
        }

        public boolean getIsConditional() {
            return isConditional;
        }

        public void setIsConditional(boolean isConditional) {
            this.isConditional = isConditional;
        }

        @Override
        public int hashCode() {
            return Objects.hash(users, groups, roles, accessTypes, maskInfo, isConditional);
        }

        @Override
        public boolean equals(Object other) {
            if (this == other) {
                return true;
            } else if (other == null || getClass() != other.getClass()) {
                return false;
            } else {
                DataMaskResult that = (DataMaskResult) other;

                return Objects.equals(users, that.users) &&
                        Objects.equals(groups, that.groups) &&
                        Objects.equals(roles, that.roles) &&
                        Objects.equals(accessTypes, that.accessTypes) &&
                        Objects.equals(maskInfo, that.maskInfo) &&
                        isConditional == that.isConditional;
            }
        }

        @Override
        public String toString() {
            return toString(new StringBuilder()).toString();
        }

        public StringBuilder toString(StringBuilder sb) {
            sb.append("{");

            if (users != null && !users.isEmpty()) {
                sb.append("users:[");
                for (String user : users) {
                    sb.append(user).append(' ');
                }
                sb.append("] ");
            }

            if (groups != null && !groups.isEmpty()) {
                sb.append("groups:[");
                for (String group : groups) {
                    sb.append(group).append(' ');
                }
                sb.append("] ");
            }

            if (roles != null && !roles.isEmpty()) {
                sb.append("roles:[");
                for (String role : roles) {
                    sb.append(role).append(' ');
                }
                sb.append("] ");
            }

            if (accessTypes != null && !accessTypes.isEmpty()) {
                sb.append("accessTypes:[");
                for (String accessType : accessTypes) {
                    sb.append(accessType).append(' ');
                }
                sb.append("] ");
            }

            sb.append("maskInfo=");
            maskInfo.toString(sb);
            sb.append(" isConditional=").append(isConditional);

            sb.append("}");

            return sb;
        }
    }

    @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class RowFilterResult implements Serializable {
        private static final long serialVersionUID = 1L;

        private final Set<String>                   users;
        private final Set<String>                   groups;
        private final Set<String>                   roles;
        private final Set<String>                   accessTypes;
        private final RangerPolicyItemRowFilterInfo filterInfo;
        private       boolean                       isConditional;

        public RowFilterResult(Set<String> users, Set<String> groups, Set<String> roles, Set<String> accessTypes, RangerPolicyItemRowFilterInfo filterInfo) {
            this.users       = users;
            this.groups      = groups;
            this.roles       = roles;
            this.accessTypes = accessTypes;
            this.filterInfo  = filterInfo;
        }

        public RowFilterResult(RowFilterResult that) {
            this.users         = that.users;
            this.groups        = that.groups;
            this.roles         = that.roles;
            this.accessTypes   = that.accessTypes;
            this.filterInfo    = that.filterInfo;
            this.isConditional = that.isConditional;
        }

        public Set<String> getUsers() {
            return users;
        }

        public Set<String> getGroups() {
            return groups;
        }

        public Set<String> getRoles() {
            return roles;
        }

        public Set<String> getAccessTypes() {
            return accessTypes;
        }

        public RangerPolicyItemRowFilterInfo getFilterInfo() {
            return filterInfo;
        }

        public boolean getIsConditional() {
            return isConditional;
        }

        public void setIsConditional(boolean isConditional) {
            this.isConditional = isConditional;
        }

        @Override
        public int hashCode() {
            return Objects.hash(users, groups, roles, accessTypes, filterInfo, isConditional);
        }

        @Override
        public boolean equals(Object other) {
            if (this == other) {
                return true;
            } else if (other == null || getClass() != other.getClass()) {
                return false;
            } else {
                RowFilterResult that = (RowFilterResult) other;

                return Objects.equals(users, that.users) &&
                        Objects.equals(groups, that.groups) &&
                        Objects.equals(roles, that.roles) &&
                        Objects.equals(accessTypes, that.accessTypes) &&
                        Objects.equals(filterInfo, that.filterInfo) &&
                        isConditional == that.isConditional;
            }
        }

        @Override
        public String toString() {
            return toString(new StringBuilder()).toString();
        }

        public StringBuilder toString(StringBuilder sb) {
            sb.append("{");

            if (users != null && !users.isEmpty()) {
                sb.append("users:[");
                for (String user : users) {
                    sb.append(user).append(' ');
                }
                sb.append("] ");
            }

            if (groups != null && !groups.isEmpty()) {
                sb.append("groups:[");
                for (String group : groups) {
                    sb.append(group).append(' ');
                }
                sb.append("] ");
            }

            if (roles != null && !roles.isEmpty()) {
                sb.append("roles:[");
                for (String role : roles) {
                    sb.append(role).append(' ');
                }
                sb.append("] ");
            }

            if (accessTypes != null && !accessTypes.isEmpty()) {
                sb.append("accessTypes:[");
                for (String accessType : accessTypes) {
                    sb.append(accessType).append(' ');
                }
                sb.append("] ");
            }

            sb.append("filterInfo=");
            filterInfo.toString(sb);
            sb.append(" isConditional=").append(isConditional);

            sb.append("}");

            return sb;
        }
    }
}
